#include "StdAfx.h"
#include <boost/lambda/control_structures.hpp>
#include "../Engine/Map.hpp"
#include "../Engine/TerrainTile.hpp"

namespace he
{
	Map::Map(unsigned short Width, unsigned short Height, std::string const &MapType)
		: m_Width(Width), m_Height(Height), m_MapType(MapType), m_Tiles(nullptr)
	{
		// Create tiles
		create_tiles();
	}

	Map::~Map()
	{
		for(int y = 0; y < get_height(); ++y)
		{
			for(int x = 0; x < get_width(); ++x)
				delete m_Tiles[x + y * get_width()];
		}
		
		delete[] m_Tiles;
	}

	void Map::create_tiles()
	{
		assert(m_Tiles == nullptr);
		m_Tiles = new TerrainTile*[get_width() * get_height()];
		// Create tiles
		for(int y = 0; y < get_height(); ++y)
		{
			for(int x = 0; x < get_width(); ++x)
			{
				// Create tile
				m_Tiles[x + y * get_width()] = new TerrainTile(x, y);
			}
		}
	}

	float Map::deep_func(float X1, float X2, float Ratio, float Deep)
	{
		float const inter = Ratio * abs(Deep) * (0.5f - 0.78f * (powf(X1, 2.0f) + powf(X2, 2.0f)));
		return (inter <= 0 ? 0.f : inter);
	}

	unsigned short Map::get_width() const
	{
		return m_Width;
	}
		
	unsigned short Map::get_height() const
	{
		return m_Height;
	}

	TerrainTile &Map::get_tile(unsigned short X, unsigned short Y) const
	{
		return *(m_Tiles[X + Y * get_width()]);
	}

	TerrainTile *Map::get_neighbor(TerrainTile const &Tile, TileWalker::Direction Direction) const
	{
		std::pair<int, int> const dest = TileWalker::get_tile(Tile.get_x(), Tile.get_y(), Direction);

		return (dest.first >= 0 && dest.second >= 0 && dest.first < m_Width && dest.second < m_Height) ? &get_tile(dest.first, dest.second) : nullptr;
	}

	void Map::get_all_neighbors(TerrainTile const &Tile, TileContainer_t &Container) const
	{
		// Traverse all neighbors tiles
		for(TileWalker::Direction direction = TileWalker::NORTH; direction <= TileWalker::NORTH_WEST; direction = static_cast<TileWalker::Direction>(static_cast<int>(direction) + 1))
		{
			std::pair<int, int> dest = TileWalker::get_tile(Tile.get_x(), Tile.get_y(), direction);
			// Add it to container if tile exist
			Container.push_back((dest.first >= 0 && dest.second >= 0 && dest.first < m_Width && dest.second < m_Height)
				? &get_tile(dest.first, dest.second) : nullptr);
		}
	}

	void Map::get_all_neighbors(TerrainTile const &Tile_, unsigned char Size, TileContainer_t &Container) const
	{
		TerrainTile const &base_tile = Tile_;
		// Traverse each tile
		std::pair<int, int> base_north_west, base_south_east;
		base_north_west = base_south_east = std::make_pair(base_tile.get_x(), base_tile.get_y());
		for(unsigned char i = 0; i <= Size; ++i, base_north_west = TileWalker::get_tile(base_north_west.first, base_north_west.second, TileWalker::NORTH_WEST), base_south_east = TileWalker::get_tile(base_south_east.first, base_south_east.second, TileWalker::SOUTH_EAST))
		{
			std::pair<int, int> current_tile_north_west = base_north_west;
			std::pair<int, int> current_tile_south_east = base_south_east;
			// For NORTH-EAST
			for(unsigned char j = 0; j <= Size; ++j)
			{
				// Valid tile for NORTH-WEST
				if(current_tile_north_west.first >= 0 && current_tile_north_west.second >= 0 && current_tile_north_west.first < m_Width && current_tile_north_west.second < m_Height)
					Container.push_back(&get_tile(current_tile_north_west.first, current_tile_north_west.second));
				// Valid for SOUTH-WEST
				if(current_tile_south_east.first >= 0 && current_tile_south_east.second >= 0 && current_tile_south_east.first < m_Width && current_tile_south_east.second < m_Height)
					Container.push_back(&get_tile(current_tile_south_east.first, current_tile_south_east.second));
				current_tile_north_west = TileWalker::get_tile(current_tile_north_west.first, current_tile_north_west.second, TileWalker::NORTH_EAST);
				current_tile_south_east = TileWalker::get_tile(current_tile_south_east.first, current_tile_south_east.second, TileWalker::NORTH_EAST);
			}
			// For SOUTH-WEST
			current_tile_north_west = base_north_west;
			current_tile_south_east = base_south_east;
			for(unsigned char j = 0; j <= Size; ++j)
			{
				// Valid tile
				if(current_tile_north_west.first >= 0 && current_tile_north_west.second >= 0 && current_tile_north_west.first < m_Width && current_tile_north_west.second < m_Height)
					Container.push_back(&get_tile(current_tile_north_west.first, current_tile_north_west.second));
				if(current_tile_south_east.first >= 0 && current_tile_south_east.second >= 0 && current_tile_south_east.first < m_Width && current_tile_south_east.second < m_Height)
					Container.push_back(&get_tile(current_tile_south_east.first, current_tile_south_east.second));
				current_tile_north_west = TileWalker::get_tile(current_tile_north_west.first, current_tile_north_west.second, TileWalker::SOUTH_WEST);
				current_tile_south_east = TileWalker::get_tile(current_tile_south_east.first, current_tile_south_east.second, TileWalker::SOUTH_WEST);
			}
		}
	}

	void Map::get_all_outline_neighbors(TerrainTile const &TileBase, unsigned char Size, TileContainer_t &Container) const
	{
		TerrainTile const &base_tile = TileBase;
		// Get the base tile in each needed direction
		std::pair<int, int> base_nw_se = TileWalker::get_tile(base_tile.get_x(), base_tile.get_y(), TileWalker::NORTH_WEST, Size + 1),
			base_sw_ne = TileWalker::get_tile(base_tile.get_x(), base_tile.get_y(), TileWalker::SOUTH_WEST, Size + 1);
		for(unsigned char i = 0, max_size = (Size + 1) * 2; i <= max_size; ++i, base_nw_se = TileWalker::get_tile(base_nw_se.first, base_nw_se.second, TileWalker::SOUTH_EAST), base_sw_ne = TileWalker::get_tile(base_sw_ne.first, base_sw_ne.second, TileWalker::NORTH_EAST))
		{
			// Get the tile in the NORTH-EAST direction
			std::pair<int, int> tile_plus_size = TileWalker::get_tile(base_nw_se.first, base_nw_se.second, TileWalker::NORTH_EAST, Size + 1);
			if(tile_plus_size.first >= 0 && tile_plus_size.second >= 0)
				Container.push_back(&get_tile(tile_plus_size.first, tile_plus_size.second));
			// Get the tile for the SOUTH-WEST direction
			tile_plus_size = TileWalker::get_tile(
				base_nw_se.first, base_nw_se.second, 
				TileWalker::SOUTH_WEST, Size + 1);
			if(tile_plus_size.first >= 0 && tile_plus_size.second >= 0)
				Container.push_back(&get_tile(tile_plus_size.first, tile_plus_size.second));
			// Get the tile for the NORTH-WEST direction
			tile_plus_size = TileWalker::get_tile(
				base_sw_ne.first, base_sw_ne.second, 
				TileWalker::NORTH_WEST, Size + 1);
			if(tile_plus_size.first >= 0 && tile_plus_size.second >= 0)
				Container.push_back(&get_tile(tile_plus_size.first, tile_plus_size.second));
			// Get the tile for the SOUTH-EAST direction
			tile_plus_size = TileWalker::get_tile(
				base_sw_ne.first, base_sw_ne.second, 
				TileWalker::SOUTH_EAST, Size + 1);
			if(tile_plus_size.first >= 0 && tile_plus_size.second >= 0)
				Container.push_back(&get_tile(tile_plus_size.first, tile_plus_size.second));
		}
	}

	void Map::set_tile_deep(TerrainTile const &Tile_, char Deep, unsigned char Size, float Ratio)
	{
		assert(Size > 0 && "Bad size");
		typedef std::set<TerrainTile *> TileSet_t;
		// List of tiles that need to recompute normals
		TerrainTile const &base_tile = Tile_;
		// Traverse each tile
		std::pair<int, int> base_north_west, base_south_east;
		base_north_west = base_south_east = 
			std::make_pair(base_tile.get_x(), base_tile.get_y());
		for(unsigned char i = 0; i <= Size; ++i, base_north_west = TileWalker::get_tile(base_north_west.first, base_north_west.second, TileWalker::NORTH_WEST), base_south_east = TileWalker::get_tile(base_south_east.first, base_south_east.second, TileWalker::SOUTH_EAST))
		{
			std::pair<int, int> current_tile_north_west = base_north_west;
			std::pair<int, int> current_tile_south_east = base_south_east;
			// For NORTH-EAST
			for(unsigned char j = 0; j <= Size; ++j)
			{
				float tile_deep = deep_func(float(i) / Size, float(j) / Size, Ratio, Deep);
				tile_deep = (Deep < 0) ? -tile_deep : tile_deep;
				// Valid tile for NOTH-WEST
				if(current_tile_north_west.first >= 0 && current_tile_north_west.second >= 0 && current_tile_north_west.first < m_Width && current_tile_north_west.second < m_Height)
				{
					TerrainTile &tile = get_tile(current_tile_north_west.first, current_tile_north_west.second);
					// Only set valid vertex
					if(i != Size && j != Size)
					{
						tile.set_deep(0, tile_deep, false, false);
						// Get neighbors
						TileContainer_t near_neighbors;
						get_all_neighbors(tile, near_neighbors);
						// Update vertices of neighbors also
						if(near_neighbors.at(0) != nullptr)
							near_neighbors.at(0)->set_deep(2, tile_deep, false, false);
						if(near_neighbors.at(1) != nullptr)
							near_neighbors.at(1)->set_deep(1, tile_deep, false, false);
						if(near_neighbors.at(7) != nullptr)
							near_neighbors.at(7)->set_deep(3, tile_deep, false, false);
					}
				}
				// Valid for SOUTH-WEST
				if(current_tile_south_east.first >= 0 && current_tile_south_east.second >= 0 && current_tile_south_east.first < m_Width && current_tile_south_east.second < m_Height)
				{
					TerrainTile &tile = get_tile(current_tile_south_east.first, current_tile_south_east.second);
					// Only set valid vertex
					if(i != Size && j != Size)
					{
						tile.set_deep(3, tile_deep, false, false);
						// Get neighbors
						TileContainer_t near_neighbors;
						get_all_neighbors(tile, near_neighbors);
						// Update vertices of neighbors also
						if(near_neighbors.at(1) != nullptr)
							near_neighbors.at(1)->set_deep(2, tile_deep, false, false);
						if(near_neighbors.at(2) != nullptr)
							near_neighbors.at(2)->set_deep(1, tile_deep, false, false);
						if(near_neighbors.at(3) != nullptr)
							near_neighbors.at(3)->set_deep(0, tile_deep, false, false);
					}
				}
				current_tile_north_west = TileWalker::get_tile(current_tile_north_west.first,
					current_tile_north_west.second, TileWalker::NORTH_EAST);
				current_tile_south_east = TileWalker::get_tile(current_tile_south_east.first,
					current_tile_south_east.second, TileWalker::NORTH_EAST);
			}
			// For SOUTH-WEST
			current_tile_north_west = base_north_west;
			current_tile_south_east = base_south_east;
			for(unsigned char j = 0; j <= Size; ++j)
			{
				float tile_deep = deep_func(float(i) / Size, float(j) / Size, Ratio, Deep);
				tile_deep = (Deep < 0) ? -tile_deep : tile_deep;
				// Valid tile
				if(current_tile_north_west.first >= 0 && current_tile_north_west.second >= 0 && current_tile_north_west.first < m_Width && current_tile_north_west.second < m_Height)
				{
					TerrainTile &tile = get_tile(current_tile_north_west.first, current_tile_north_west.second);
					// Only set valid vertex
					if(i != Size && j != Size)
					{
						tile.set_deep(1, tile_deep, false, false);
						// Get neighbors
						TileContainer_t near_neighbors;
						get_all_neighbors(tile, near_neighbors);
						// Update vertices of neighbors also
						if(near_neighbors.at(7) != nullptr)
							near_neighbors.at(7)->set_deep(2, tile_deep, false, false);
						if(near_neighbors.at(6) != nullptr)
							near_neighbors.at(6)->set_deep(3, tile_deep, false, false);
						if(near_neighbors.at(5) != nullptr)
							near_neighbors.at(5)->set_deep(0, tile_deep, false, false);
					}
				}
				if(current_tile_south_east.first >= 0 && current_tile_south_east.second >= 0 && current_tile_south_east.first < m_Width && current_tile_south_east.second < m_Height)
				{
					TerrainTile &tile = get_tile(current_tile_south_east.first, current_tile_south_east.second);
					// Only set valid vertex
					if(i != Size && j != Size)
					{
						tile.set_deep(2, tile_deep, false, false);
						// Get neighbors
						TileContainer_t near_neighbors;
						get_all_neighbors(tile, near_neighbors);
						// Update vertices of neighbors also
						if(near_neighbors.at(5) != nullptr)
							near_neighbors.at(5)->set_deep(3, tile_deep, false, false);
						if(near_neighbors.at(4) != nullptr)
							near_neighbors.at(4)->set_deep(0, tile_deep, false, false);
						if(near_neighbors.at(3) != nullptr)
							near_neighbors.at(3)->set_deep(1, tile_deep, false, false);
					}
				}
				current_tile_north_west = TileWalker::get_tile(current_tile_north_west.first,
					current_tile_north_west.second, TileWalker::SOUTH_WEST);
				current_tile_south_east = TileWalker::get_tile(current_tile_south_east.first,
					current_tile_south_east.second, TileWalker::SOUTH_WEST);
			}
		}
	}

	void Map::set_tile_terrain_type(TerrainTile &Tile, unsigned short TerrainType)
	{
		// Set terrain type for base tile
		Tile.set_terrain_type_for_vertex(0, TerrainType);
		Tile.set_terrain_type_for_vertex(1, TerrainType);
		Tile.set_terrain_type_for_vertex(2, TerrainType);
		Tile.set_terrain_type_for_vertex(3, TerrainType);
		// Must set for neighbors
		TileContainer_t near_neighbors;
		get_all_neighbors(Tile, near_neighbors);
		if(near_neighbors.at(0) != nullptr)
			near_neighbors.at(0)->set_terrain_type_for_vertex(2, TerrainType);
		if(near_neighbors.at(1) != nullptr)
		{
			near_neighbors.at(1)->set_terrain_type_for_vertex(1, TerrainType);
			near_neighbors.at(1)->set_terrain_type_for_vertex(2, TerrainType);
		}
		if(near_neighbors.at(2) != nullptr)
			near_neighbors.at(2)->set_terrain_type_for_vertex(1, TerrainType);
		if(near_neighbors.at(3) != nullptr)
		{
			near_neighbors.at(3)->set_terrain_type_for_vertex(0, TerrainType);
			near_neighbors.at(3)->set_terrain_type_for_vertex(1, TerrainType);
		}
		if(near_neighbors.at(4) != nullptr)
			near_neighbors.at(4)->set_terrain_type_for_vertex(0, TerrainType);
		if(near_neighbors.at(5) != nullptr)
		{
			near_neighbors.at(5)->set_terrain_type_for_vertex(0, TerrainType);
			near_neighbors.at(5)->set_terrain_type_for_vertex(3, TerrainType);
		}
		if(near_neighbors.at(6) != nullptr)
			near_neighbors.at(6)->set_terrain_type_for_vertex(3, TerrainType);
		if(near_neighbors.at(7) != nullptr)
		{
			near_neighbors.at(7)->set_terrain_type_for_vertex(2, TerrainType);
			near_neighbors.at(7)->set_terrain_type_for_vertex(3, TerrainType);
		}
		

	}

	void Map::export_map(std::ostream &Stream) const
	{

		// Write the map dimensions
		Stream << get_width() << ' ' << get_height() << ' ' << get_map_type().size() << ' ' << get_map_type() << std::endl;
		// Traverse each tile
		for(int y = 0; y < get_height(); ++y)
		{
			for(int x = 0; x < get_width(); ++x)
			{
				// Get tile
				TerrainTile const &tile = get_tile(x, y);
				// 0. vertex
				Stream << tile.vertex_deep(0) << ' ';
				Stream << short(tile.get_terrain_type(0)) << ' ';
				// 1. Vertex
				Stream << tile.vertex_deep(1) << ' ';
				Stream << short(tile.get_terrain_type(1)) << ' ';
				// 2. vertex always new
				Stream << tile.vertex_deep(2) << ' ';
				Stream << short(tile.get_terrain_type(2)) << ' ';
				// 3. vertex
				Stream << tile.vertex_deep(3) << ' ';
				Stream << short(tile.get_terrain_type(3)) << ' ';
				// Write variant index
				Stream << short(tile.get_terrain_type_variant_index()) << ' ';				
			}
			// End of line
			Stream << '\n';
		}
	}

	MapPtr_t Map::create_map(unsigned short Width, unsigned short Height, std::string const &MapType)
	{
		MapPtr_t p_map(new Map(Width, Height, MapType));
		// Initialize random generator
		//		srand(time(0));
		srand(0);
		// Set the default material
		char const default_type = 0;
		// Second loop to set values
		for(int y = 0; y < p_map->get_height(); ++y)
		{
			for(int x = 0; x < p_map->get_width(); ++x)
			{
				// Set vertices to tile
				p_map->get_tile(x, y).set_vertex_deep(0, 0, 0, 0);
				// Set terrain type
				p_map->get_tile(x, y).set_terrain_type_for_vertex(0, default_type);
				p_map->get_tile(x, y).set_terrain_type_for_vertex(1, default_type);
				p_map->get_tile(x, y).set_terrain_type_for_vertex(2, default_type);
				p_map->get_tile(x, y).set_terrain_type_for_vertex(3, default_type);
				// Set tile variant
				{
					int x_pos_corner = ((y % 2) * 1) + 1 + x * 2;
					int y_pos_corner = y + 1;
					// Set position
					boost::tuple<int, int> pos_n(x_pos_corner, y_pos_corner - 1),
						pos_w(x_pos_corner - 1, y_pos_corner),
						pos_s(x_pos_corner, y_pos_corner + 1),
						pos_e(x_pos_corner + 1, y_pos_corner);
					// Compute index
					unsigned short index = rand() % 7;
					// Set tile variant index
					p_map->get_tile(x, y).set_terrain_index(index);
				}
			}
		}
		// Set some default deep
		p_map->set_tile_deep(p_map->get_tile(1, 2), 63, 1, 1.0);
		p_map->set_tile_deep(p_map->get_tile(5, 8), 63, 3, 2.0);
		p_map->set_tile_deep(p_map->get_tile(3, 19), -63, 3, 1.0);

		char const other_type = 1;
		p_map->set_tile_terrain_type(p_map->get_tile(3, 2), other_type);
		p_map->set_tile_terrain_type(p_map->get_tile(1, 8), other_type);
		p_map->set_tile_terrain_type(p_map->get_tile(1, 14), other_type);
		p_map->set_tile_terrain_type(p_map->get_tile(1, 15), other_type);

		return p_map;
	}

	MapPtr_t Map::create_map(std::istream &Stream)
	{
		unsigned short width, height;
		unsigned short holder;
		char buffer[64];
		// Get the map dimensions
		Stream >> width >> height >> holder;
		// Get space
		Stream.get();
		// Get the map type
		Stream.read(buffer, holder);
		buffer[holder] = '\0';
		// Create map
		MapPtr_t p_map(new Map(width, height, std::string(buffer)));
		// Traverse each tile
		for(int y = 0; y < p_map->get_height(); ++y)
		{
			for(int x = 0; x < p_map->get_width(); ++x)
			{
				// Vertices deep
				float pv0, pv1, pv2, pv3;
				// Terrain types.
				unsigned short p_type_v0, p_type_v1, p_type_v2, p_type_v3;
				// Tile type variant
				short variant_holder;
				// Get tile
				TerrainTile &tile = p_map->get_tile(x, y);
				// Get all neighbors
				TileContainer_t neigh_container;
				p_map->get_all_neighbors(p_map->get_tile(x, y), neigh_container);
				// 0. vertex
				Stream >> pv0;
				Stream >> p_type_v0;
				// 1. Vertex
				Stream >> pv1;
				Stream >> p_type_v1;
				// 2. vertex
				Stream >> pv2;
				Stream >> p_type_v2;
				// 3. vertex
				Stream >> pv3;
				Stream >> p_type_v3;
				// Get variant
				Stream >> variant_holder;
				tile.set_terrain_index(variant_holder);
				tile.set_vertex_deep(pv0, pv1, pv2, pv3);
				tile.set_terrain_type_for_vertex(0, p_type_v0);
				tile.set_terrain_type_for_vertex(1, p_type_v1);
				tile.set_terrain_type_for_vertex(2, p_type_v2);
				tile.set_terrain_type_for_vertex(3, p_type_v3);
			}
		}
		// Get new line
		Stream.get();
		Stream.get();
		// Must be end of file
		if(Stream.peek() != std::ifstream::traits_type::eof())
			throw std::runtime_error("End of file not reached.");

		return p_map;
	}
}